/*
   Copyright 2009 Abdulla Abdurakhmanov (abdulla@latestbit.com)
   Original sources are available at www.latestbit.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package org.latestbit.smartobjects.smartidlc;

import groovy.lang.GroovyClassLoader;


import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.antlr.runtime.*;

import org.latestbit.smartobjects.smartidlc.beautifiers.AstyleBeautifier;
import org.latestbit.smartobjects.smartidlc.model.*;
import org.latestbit.smartobjects.smartidlc.parser.*;


public class Compiler {
	public static final String COMPILER_NAME = "SmartIDL Compiler";
	public static final String VERSION = "0.5.0";
	public static final String COPYRIGHTS = " (c) 2009 Abdulla G. Abdurakhmanov (abdulla@latestbit.com)";

	private CompilerOptions currentOptions = null;
	private Map <String, IDLNamespace> loadedNamespaces = new HashMap < String, IDLNamespace > ();
	private IDLNamespace targetNamespace;
	GroovyClassLoader groovyClassLoader;
	private IMessageReporter reporter;
	private ICodeBeautifier codeBeautifier = null;

	public synchronized boolean start(CompilerOptions options, IMessageReporter reporter) {
		return start(options, reporter, null);
	}
	
	public synchronized boolean start(CompilerOptions options, IMessageReporter reporter, ICodeBeautifier inputCodeBeautifier) {
		this.currentOptions = options;
		this.reporter = reporter;
		loadedNamespaces.clear();
		if(this.currentOptions.getTargetModulesPath() == null) {
			URL classLocation = this.getClass().getResource("");
			String locationDir = null;
			if(classLocation.getProtocol().equals("jar")) {
				String[] locationParts = classLocation.getPath().split("!");
				locationDir = locationParts[0].substring(0, locationParts[0].lastIndexOf("/"));
				locationDir = locationDir.substring( locationParts[0].indexOf("/") );								
			}
			else {
				locationDir = classLocation.getPath().substring( classLocation.getPath().indexOf("/") );
				locationDir = classLocation.getPath().substring(0, classLocation.getPath().lastIndexOf("/org/") );
			}
			locationDir=locationDir+File.separator+".."+File.separator+"target-modules";
			this.currentOptions.setTargetModulesPath(locationDir);			
		}
		reporter.onVerboseMessage("Target Modules Path: "+this.currentOptions.getTargetModulesPath());
		
		if(!this.currentOptions.isDisableBeautifier()) {
			if(inputCodeBeautifier==null)
				// Trying to find default external code formatter/beautifier
				codeBeautifier = createDefaultCodeBeautifier();
		}
		groovyClassLoader = new GroovyClassLoader();
		groovyClassLoader.addClasspath(this.currentOptions.getTargetModulesPath());
		targetNamespace = null;
		try {
			reporter.onMessage("");
			reporter.onMessage("Trying to compile: "+this.currentOptions.getFileName());
			reporter.onMessage("Phase I: Parsing IDL's...");
			reporter.onVerboseMessage("-> Parsing source file");
			parseInputFile();
			if (this.currentOptions.getImportedPath()!=null) {
				reporter.onVerboseMessage("-> Parsing imported namespaces");
				parseImportedNamespaces();
			}
			reporter.onMessage("Phase II: Linking IDL references");
			linkReferences();
			reporter.onMessage("Phase III: Generating target binding for "+options.getTarget());
			generateTarget();
			reporter.onMessage("Finished to successfully compile: "+this.currentOptions.getFileName());
			reporter.onMessage("");
		}
		catch(IOException ex) {
			reporter.onError("Unable to compile "+this.currentOptions.getFileName()+": Input/Output exception: "+ex);			
			reporter.onError(ex);
			return false;
		} 
		catch (IDLParserException ex) {
			reporter.onError("Unable to compile "+this.currentOptions.getFileName()+": IDL parser exception: "+ex);
			return false;
		}
		catch(IDLLinkerException ex) {
			reporter.onError("Unable to compile "+this.currentOptions.getFileName()+": IDL reference linker exception: "+ex);
			return false;
		}
		catch(Exception ex) {
			reporter.onError("Unable to compile "+this.currentOptions.getFileName()+": Compiler Exception: "+ex);
			reporter.onError(ex);
			return false;
		}
		return true;
	}

	private ICodeBeautifier createDefaultCodeBeautifier() {
		try {
			// Uncrustify has critical bugs with C++ templates. Changed to astyle
			/* return new UncrustifyBeautifier(
					this.currentOptions.getTargetModulesPath()+File.separator
					+"beautifier"+File.separator, 
					this.reporter
			); */
			return new AstyleBeautifier(
					this.currentOptions.getTargetModulesPath()+File.separator
					+"beautifier"+File.separator, 
					this.reporter					
			);
		}
		catch(Exception ex) {
			reporter.onMessage("Warning! Unable to found beautifier tool - code beautifier is disabled");
			return null;
		}
	}

	private void parseInputFile() throws IOException, IDLParserException {
		targetNamespace = compileIdl ( this.currentOptions.getFileName() );
	}

	private IDLNamespace compileIdl(String fileName) throws IOException, IDLParserException {
		CharStream input = new ANTLRFileStream(fileName);
        smartidlLexer lex = new smartidlLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lex);
        smartidlParser parser = new smartidlParser(tokens);	
        try {
	        smartidlParser.startCompilation_return parserResult = parser.startCompilation();
	        return parserResult.namespace;
        }
		catch(RecognitionException ex) {
			parser.reportError (ex);
			throw new IDLParserException("Unable to parse the specified IDL: "+fileName, ex);
		}
	}
	
	private void loadNamespace (IDLNamespace srcNamespace) {
		IDLNamespace existNamespace = loadedNamespaces.get( srcNamespace.getName() );
		if ( existNamespace != null ) {
			existNamespace.merge ( srcNamespace );
		}			
		else {
			loadedNamespaces.put ( srcNamespace.getName(), srcNamespace );
		}
	}

	private void parseImportedNamespaces() throws IOException, IDLParserException {
		String namespaces[] = this.currentOptions.getImportedPath().split(";");
		for(String namespace : namespaces) {
			reporter.onVerboseMessage("-> Trying to import "+namespace);
			File importingFile = new File(namespace);
			if(importingFile.isDirectory()) {
				// Trying to import all interfaces in the directory
				final File sourceIdlFile = new File(this.currentOptions.getFileName());
				File[] importingFiles = importingFile.listFiles(
					new FileFilter() {

						@Override
						public boolean accept(File pathname) {
							if (pathname.getName().endsWith(".sidl")) {
								if ( ! pathname.equals(sourceIdlFile) )
									return true;
								else
									return false;
							}
							else
								return false;
						}
						
					}
				);
				for (File file : importingFiles) {
					reporter.onVerboseMessage("---> (importing) "+file.getName());
					loadNamespace ( this.compileIdl(file.getPath()) );
				}
			}
			else {
				reporter.onVerboseMessage("---> (importing) "+importingFile.getName());
				loadNamespace ( this.compileIdl(importingFile.getPath()) );
			}
		}
	}
	
	private void linkReferences() throws IOException, IDLLinkerException, Exception {
		Class<?> idlLinkerClass = groovyClassLoader.parseClass (
			new File (
					this.currentOptions.getTargetModulesPath() + 
					File.separator+"commons" + File.separator + 
					"IDLReferenceLinker.groovy" 
			)
		);
		
		Object idlLinkerScript = idlLinkerClass.newInstance();
		IIDLReferenceLinker linker = (IIDLReferenceLinker) idlLinkerScript;
		linker.start(targetNamespace, loadedNamespaces, this.currentOptions, reporter);
	}
	
	private void generateTarget() throws IOException, Exception {		
		Class<?> targetModuleClass = groovyClassLoader.parseClass (
			// new File ( "target-modules/targets/java/Module.groovy" )
			new File (
					this.currentOptions.getTargetModulesPath() + 
					File.separator+"targets" + File.separator + 
					this.currentOptions.getTarget() + File.separator + 
					"Module.groovy" 
			)
		);
		Object targetModuleScript = targetModuleClass.newInstance();

		ITargetLangModule targetModule = (ITargetLangModule) targetModuleScript;
		targetModule.setModulesRootDir(this.currentOptions.getTargetModulesPath());
		targetModule.setOutputDir(this.currentOptions.getOutputDir());
		targetModule.setCompilerOptions(this.currentOptions);
		targetModule.setMessageReporter(reporter);
		
		if( this.currentOptions.getTargetArgs()!=null ) {
			String[] args = this.currentOptions.getTargetArgs().split(";");
			Properties pArgs = new Properties();
			for(String arg : args) {
				String[] keyVal = arg.split("=");
				if(keyVal.length > 0) {
					pArgs.put(keyVal[0], keyVal.length > 1 ? keyVal[1] : null);
				}
			}
			targetModule.setTargetArgs(pArgs);
		}
		if( codeBeautifier!=null && codeBeautifier.isLanguageSupported( targetModule.getTargetLanguageName()) )
			targetModule.setCodeBeautifier(codeBeautifier);
		targetModule.processNamespace(targetNamespace, loadedNamespaces);		
	}	
}
